package com.xiaozhu.aop;

import com.xiaozhu.anno.AiLogExecute;
import com.xiaozhu.anno.ChatType;
import com.xiaozhu.common.aiconstant.ExecuteConstants;
import com.xiaozhu.enumreation.ModelTypeEnum;
import com.xiaozhu.enumreation.RedisKeyEnum;
import com.xiaozhu.enumreation.RoleRedisEnum;
import com.xiaozhu.pojo.dto.ChatDto;
import com.xiaozhu.pojo.entity.ChatAiLog;
import com.xiaozhu.pojo.entity.ChatContent;
import com.xiaozhu.pojo.entity.ChatSession;
import com.xiaozhu.repository.ChatAiLogRepository;
import com.xiaozhu.repository.ChatContentRepository;
import com.xiaozhu.repository.ChatSessionHistoryRepository;
import com.xiaozhu.repository.ChatTypeHistoryRepository;
import com.xiaozhu.utils.BeanContext;
import com.xiaozhu.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * LogAspect - AOP 日志切面（核心业务增强组件）
 *
 * =====================================================
 *  核心功能
 * =====================================================
 * 本切面通过 AOP（面向切面编程）拦截所有 Controller 层的方法调用，
 * 自动完成以下功能，实现业务代码与日志记录的解耦：
 *
 * 1. 自动保存 AI 请求与回复日志到 ai_log 表
 *    - 记录用户提示词（user_message）
 *    - 记录 AI 回复内容（assistant_message）
 *    - 记录使用的模型类型（OpenAI、DeepSeek 等）
 *    - 记录会话 ID、用户 ID、时间戳等元信息
 *
 * 2. 自动保存聊天内容到 chat_content 表
 *    - 用户消息（role=user）
 *    - AI 助手回复（role=assistant）
 *    - 关联到对应的会话（session_id）
 *
 * 3. 同步缓存到 Redis
 *    - 用户消息缓存：chat:content:user
 *    - AI 回复缓存：chat:content:assistant
 *    - 提升查询性能，减少数据库压力
 *
 * 4. 支持多种响应类型
 *    - Mono<?>：单个异步响应（如 ChatGPT API）
 *    - Flux<?>：流式异步响应（如流式输出）
 *    - 普通同步响应
 *
 * =====================================================
 *  工作流程
 * =====================================================
 * 1. 拦截 Controller 方法（@Around 环绕通知）
 * 2. 检查是否需要记录日志（@AiLogExecute 注解）
 * 3. 解析方法参数（ChatDto）
 * 4. 执行原 Controller 方法
 * 5. 构建日志对象（ChatAiLog、ChatContent）
 * 6. 绑定会话 ID（关联 ChatSession）
 * 7. 保存用户输入到数据库和 Redis
 * 8. 根据响应类型处理 AI 回复：
 *    - Mono：等待完成后保存
 *    - Flux：收集所有数据后保存
 *    - 同步：直接保存
 * 9. 记录执行耗时
 *
 * =====================================================
 *  技术特点
 * =====================================================
 * 1. 非侵入式设计
 *    - 业务代码无需关心日志保存逻辑
 *    - 通过注解控制是否记录日志
 *
 * 2. 响应式编程支持
 *    - 完美兼容 Spring WebFlux
 *    - 不阻塞主线程
 *    - 异步保存日志
 *
 * 3. 异常容错
 *    - 日志保存失败不影响业务流程
 *    - 空内容自动跳过保存
 *    - 超时保护（Mono 120秒）
 *
 * 4. 性能优化
 *    - 双写策略（数据库 + Redis）
 *    - 异步处理，不阻塞响应
 *
 * =====================================================
 *  使用示例
 * =====================================================
 * 在 Controller 中使用：
 *
 * ```java
 * @AiLogExecute("chat")  // 标记需要记录日志
 * public class ChatController {
 *
 *     @ChatType("deepseek")  // 标记使用的模型
 *     public Flux<String> chat(ChatDto chatDto) {
 *         // 业务逻辑
 *         return chatService.chat(chatDto);
 *         // AOP 自动拦截并保存日志，业务代码无需关心
 *     }
 * }
 * ```
 *
 * =====================================================
 *  数据流向
 * =====================================================
 * 用户请求
 *   → Controller（被拦截）
 *   → LogAspect.around()
 *   → 解析参数、构建日志对象
 *   → 执行业务方法
 *   → 获取 AI 回复
 *   → 保存到 ai_log 表（日志记录）
 *   → 保存到 chat_content 表（聊天内容）
 *   → 缓存到 Redis（提升性能）
 *   → 返回响应给前端
 *
 * =====================================================
 *  注意事项
 * =====================================================
 * 1. Controller 必须添加 @AiLogExecute 注解才会被拦截
 * 2. 方法第一个参数必须是 ChatDto 类型
 * 3. 日志保存失败会记录错误但不会抛出异常
 * 4. Flux 响应会收集所有数据后再保存（大数据流可能内存压力大）
 * 5. Redis 缓存失败不影响数据库保存
 *
 * =====================================================
 *  最近修复
 * =====================================================
 * - [2025-11-04] 修复 findAllByType 改为 findFirstByType
 * - [2025-11-04] 添加空内容跳过保存逻辑
 * - [2025-11-04] 优化日志输出格式
 *
 * @author 朱慧军
 * @version 2.1
 * @since 2024-xx-xx
 */
@Component
@Aspect
@Slf4j
public class LogAspect {

    /**
     * 聊天类型仓库
     * 用于查询 ChatType 实体（根据 type 字段）
     */
    @Autowired
    private ChatTypeHistoryRepository chatTypeHistoryRepository;

    /**
     * 会话仓库
     * 用于查询 ChatSession 实体（根据 chatTypeId 查找所有会话）
     */
    @Autowired
    private ChatSessionHistoryRepository chatSessionHistoryRepository;

    /**
     * AI 日志仓库
     * 用于保存 AI 请求和回复的完整日志记录
     */
    @Autowired
    private ChatAiLogRepository chatAiLogRepository;

    /**
     * 聊天内容仓库
     * 用于保存用户消息和 AI 回复内容
     */
    @Autowired
    private ChatContentRepository chatContentRepository;

    /**
     * Redis 工具类
     * 用于缓存聊天内容，提升查询性能
     */
    @Autowired
    private RedisUtils redisUtils;

    /**
     * 切入点：拦截所有 controller 包及其子包中的所有方法
     * 
     * 表达式说明：
     * execution(* com.xiaozhu.controller..*.*(..))
     *   - 第一个 *：任意返回类型
     *   - com.xiaozhu.controller：包名
     *   - ..：包及其子包
     *   - 第二个 *：任意类
     *   - 第三个 *：任意方法
     *   - (..)：任意参数
     */
    @Pointcut("execution(* com.xiaozhu.controller..*.*(..))")
    public void pointcut() {}

    /**
     * 环绕通知：核心拦截逻辑
     * 
     * 执行流程：
     * 1. 检查类是否标注 @AiLogExecute 注解
     * 2. 检查注解值是否包含 "chat"（ExecuteConstants.EXECUTE）
     * 3. 解析方法上的 @ChatType 注解（获取模型类型）
     * 4. 提取方法参数（必须是 ChatDto）
     * 5. 执行原 Controller 方法
     * 6. 根据返回类型处理响应：
     *    - Mono：异步单值响应
     *    - Flux：异步流式响应
     *    - 其他：同步响应
     * 7. 保存日志和聊天内容到数据库和 Redis
     * 
     * @param joinPoint AOP 连接点，包含被拦截方法的所有信息
     * @return 原方法的返回值（可能被包装为 Mono/Flux）
     * @throws RuntimeException 如果方法执行失败
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        log.info("LogAspect 启动");

        long start = System.currentTimeMillis();
        
        // ============================================================
        // 步骤1：检查类是否需要记录日志
        // ============================================================
        // 获取被代理的目标对象（真正的业务类实例）
        Class<?> aClass = joinPoint.getTarget().getClass();
        AiLogExecute annotation1 = aClass.getAnnotation(AiLogExecute.class);
        
        // 如果类没有 @AiLogExecute 注解，不拦截，直接执行原方法
        if (annotation1 == null){
            try {
                log.info("不是目标执行对象（缺少 @AiLogExecute 注解），跳过日志记录");
                return joinPoint.proceed();
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        }
        
        // ============================================================
        // 步骤2：检查注解值是否匹配
        // ============================================================
        // 检查 @AiLogExecute 的 value 是否包含 "execute"
        // 例如：@AiLogExecute("execute")
        String[] value = annotation1.value();
        boolean b = Arrays.stream(value).map(String::toLowerCase)
                .anyMatch(string -> string.contains(ExecuteConstants.EXECUTE));
        
        if (!b){
            try {
                log.info("不是目标执行方法（注解值不匹配），跳过日志记录");
                return joinPoint.proceed();
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        }

        // ============================================================
        // 步骤3：解析方法上的注解信息
        // ============================================================
        // 获取方法签名，解析 @ChatType 注解（标识使用的 AI 模型）
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        ChatType annotation = methodSignature.getMethod().getAnnotation(ChatType.class);
        String[] modelName = annotation != null ? annotation.value() : new String[]{};

        // ============================================================
        // 步骤4：获取并验证方法参数
        // ============================================================
        Object[] args = joinPoint.getArgs();

        // 第一个参数必须是 ChatDto 类型，否则跳过
        if (args.length == 0 ||  !(args[0] instanceof ChatDto)){
            try {
                log.info("参数为空或类型不匹配（期望 ChatDto），跳过日志记录");
                return joinPoint.proceed();

            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        }
        
        ChatDto chatDto = (ChatDto) args[0];
        log.info("用户的提示词：{}",chatDto.getPrompt());
        
        // ============================================================
        // 步骤5：执行原 Controller 方法（核心业务逻辑）
        // ============================================================
        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.error("控制器执行异常", e);
            throw new RuntimeException(e);
        }

        // ============================================================
        // 步骤6：构建日志和聊天内容对象
        // ============================================================
        // 确定使用的 AI 模型（DeepSeek 或 OpenAI）
        String aiModel = getModelType(modelName);
        
        // 构建 AI 日志对象（记录到 ai_log 表）
        ChatAiLog chatAiLog = buildBaseLog(chatDto, aiModel);
        
        // 构建用户消息对象（记录到 chat_content 表）
        ChatContent chatContentUser = buildChatContent(chatDto.getPrompt(), RoleRedisEnum.ROLE_USERC.getName());
        
        // 构建 AI 回复消息对象（内容后续填充）
        ChatContent chatContentModel = buildChatContent(null, RoleRedisEnum.ROLE_MODEL.getName());

        // ============================================================
        // 步骤7：绑定会话 ID
        // ============================================================
        // 查询 ChatSession，将用户消息和 AI 回复关联到对应的会话
        bindSession(chatDto, chatContentUser, chatContentModel);

        // ============================================================
        // 步骤8：保存用户输入内容
        // ============================================================
        String redisKeyBase = RedisKeyEnum.REDIS_KEY_CONTENT.keyTyp();
        saveContent(chatContentUser, redisKeyBase + RoleRedisEnum.ROLE_USERC.getName());

        // ============================================================
        // 步骤9：根据响应类型处理 AI 回复
        // ============================================================
        // Mono：单个异步响应（如 ChatGPT API 返回一条完整消息）
        if (result instanceof Mono<?> mono) {
            return handleMonoResponse(mono, chatAiLog, chatContentModel, redisKeyBase, start);
        }

        // Flux：流式异步响应（如流式输出，逐字返回）
        if (result instanceof Flux<?> flux) {
            return handleFluxResponse(flux, chatAiLog, chatContentModel, redisKeyBase, start);
        }

        // 普通同步返回类型（如 String、对象等）
        handleNormalResponse(result, chatAiLog, chatContentModel, redisKeyBase);
        log.info("普通同步日志保存完成，耗时：{}ms", System.currentTimeMillis() - start);

        return result;
    }

    // ================= 辅助方法 ================= //

    /**
     * 获取 AI 模型名称
     * 
     * 根据方法上的 @ChatType 注解值判断使用的 AI 模型：
     * - 如果包含 "deepseek"：使用 DeepSeek 模型
     * - 否则：使用阿里云（ALY）模型
     * - 默认：DeepSeek 模型
     * 
     * @param modelType @ChatType 注解的 value 值数组
     * @return 模型名称（如 "deepseek-chat", "qwen-plus"）
     */
    private String getModelType(String[] modelType) {
        // 如果没有指定模型类型，默认使用 DeepSeek
        if (modelType == null || modelType.length == 0)
            return ModelTypeEnum.MODEL_TYPE_DEEP_SEEK.getModel();

        // 检查是否包含 "deepseek" 关键字（不区分大小写）
        boolean isDeepSeek = Arrays.stream(modelType)
                .filter(s -> !s.trim().isEmpty())
                .map(String::toLowerCase)
                .anyMatch(s -> s.contains(ModelTypeEnum.MODEL_TYPE_DEEP_SEEK.getAnno().toLowerCase()));

        // 返回对应的模型名称
        return isDeepSeek ?
                ModelTypeEnum.MODEL_TYPE_DEEP_SEEK.getModel() :
                ModelTypeEnum.MODEL_TYPE_ALY.getModel();
    }

    /**
     * 构建基础 AI 日志对象
     * 
     * 创建 ChatAiLog 实体，用于记录 AI 请求和回复的完整信息。
     * 
     * @param dto 聊天请求 DTO（包含用户输入、chatId 等）
     * @param model AI 模型名称
     * @return ChatAiLog 日志对象（assistant_message 字段待后续填充）
     */
    private ChatAiLog buildBaseLog(ChatDto dto, String model) {
        return ChatAiLog.builder()
                .aiModel(model)                                              // AI 模型名称
                .userMessage(dto.getPrompt())                                // 用户提示词
                .userName(BeanContext.getName() == null ? "无" : BeanContext.getName())  // 用户名（从上下文获取）
                .userId(BeanContext.getId() == null ? 0 : BeanContext.getId())          // 用户 ID（从上下文获取）
                .conversationId(dto.getChatId())                             // 会话 ID（前端传入的 chatId）
                .build();
    }

    /**
     * 构建 ChatContent 聊天内容对象
     * 
     * 创建 ChatContent 实体，用于记录单条聊天消息。
     * 
     * @param content 消息内容（用户输入或 AI 回复）
     * @param role 角色标识（"user" 或 "assistant"）
     * @return ChatContent 聊天内容对象（sessionId 待后续绑定）
     */
    private ChatContent buildChatContent(String content, String role) {
        return ChatContent.builder()
                .content(content)                  // 消息内容
                .role(role)                        // 角色（user 或 assistant）
                .createdAt(LocalDateTime.now())    // 创建时间
                .build();
    }

    /**
     * 绑定 ChatSession 的 sessionId
     * 
     * 根据 ChatDto 中的 type 查找对应的 ChatType，
     * 再根据 ChatType 的 ID 查找所有相关的 ChatSession，
     * 取最后一个会话的 ID，绑定到用户消息和 AI 回复对象上。
     * 
     * 工作流程：
     * 1. 根据 chatDto.getType() 查找 ChatType 实体
     * 2. 根据 ChatType.id 查找所有 ChatSession
     * 3. 取最后一个 ChatSession 的 ID
     * 4. 将 sessionId 设置到 user 和 model 对象
     * 
     * @param chatDto 聊天请求 DTO
     * @param user 用户消息对象（需要绑定 sessionId）
     * @param model AI 回复对象（需要绑定 sessionId）
     */
    private void bindSession(ChatDto chatDto, ChatContent user, ChatContent model) {
        // 根据 type 查找 ChatType 实体（使用 findFirstByType 避免多条记录异常）
        com.xiaozhu.pojo.entity.ChatType typeEntity =
                chatTypeHistoryRepository.findFirstByType(chatDto.getType());

        if (typeEntity == null) {
            log.warn("未找到 ChatType，type={}，无法绑定 sessionId", chatDto.getType());
            return;
        }

        // 根据 chatTypeId 查找所有相关的会话
        List<ChatSession> sessions = chatSessionHistoryRepository.findAllByChatTypeId(typeEntity.getId());
        if (sessions != null && !sessions.isEmpty()) {
            // 获取最后一个会话的 ID（通常是最新创建的会话）
            Integer sessionId = sessions.get(sessions.size() - 1).getId();
            user.setSessionId(sessionId);
            model.setSessionId(sessionId);
            log.debug("成功绑定 sessionId={}", sessionId);
        } else {
            log.warn("未找到 ChatSession，chatTypeId={}，无法绑定 sessionId", typeEntity.getId());
        }
    }

    /**
     * 保存 AI 日志记录
     * 
     * 将 ChatAiLog 对象保存到 ai_log 表。
     * 日志保存失败不会影响主流程，只记录错误日志。
     * 
     * @param logEntity AI 日志对象（包含用户输入和 AI 回复）
     */
    private void saveLog(ChatAiLog logEntity) {
        try {
            chatAiLogRepository.save(logEntity);
            log.info("✅ AI 日志保存成功");
        } catch (Exception e) {
            log.error("❌ AI 日志保存失败（不影响主流程）", e);
        }
    }

    /**
     * 保存聊天内容与缓存（空内容自动跳过）
     * 
     * 双写策略：
     * 1. 保存到数据库（chat_content 表）
     * 2. 缓存到 Redis（提升查询性能）
     * 
     * 注意：
     * - 空内容会自动跳过保存（避免无效数据）
     * - 保存失败不会影响主流程，只记录错误日志
     * 
     * @param content 聊天内容对象（用户消息或 AI 回复）
     * @param key Redis 缓存 key（如 "chat:content:user"）
     */
    private void saveContent(ChatContent content, String key) {
        // 空内容检查
        if (content.getContent() == null) {
            log.warn("⚠️ 跳过保存：content 为空，role={}, sessionId={}", 
                    content.getRole(), content.getSessionId());
            return;
        }
        
        try {
            // 保存到数据库
            chatContentRepository.save(content);
            // 缓存到 Redis
            redisUtils.set(key, content.getContent());
            log.info("✅ 聊天内容保存成功（DB + Redis）");
        } catch (Exception e) {
            log.error("❌ 聊天内容保存失败（不影响主流程）", e);
        }
    }

    /**
     * 处理 Mono 异步响应
     * 
     * Mono 适用场景：
     * - 单个异步响应（如 ChatGPT API 返回一条完整消息）
     * - 非流式输出
     * 
     * 处理流程：
     * 1. 设置超时时间（120秒）
     * 2. 等待 Mono 完成
     * 3. 获取 AI 回复内容
     * 4. 保存到数据库和 Redis
     * 
     * 注意：
     * - 不阻塞主线程，异步保存
     * - 超时或失败会记录日志但不中断响应
     * 
     * @param mono 异步单值响应流
     * @param logEntity AI 日志对象
     * @param modelContent AI 回复对象
     * @param keyBase Redis key 前缀
     * @param start 开始时间（用于计算耗时）
     * @return 包装后的 Mono（添加了日志保存逻辑）
     */
    private Mono<?> handleMonoResponse(Mono<?> mono, ChatAiLog logEntity, ChatContent modelContent, String keyBase, long start) {
        return mono.timeout(Duration.ofSeconds(120))  // 超时保护
                .doOnSuccess(r -> {
                    // 获取 AI 回复内容
                    String content = r != null ? r.toString() : "（无响应）";
                    log.info("🤖 大模型回复内容：{}", content);
                    
                    // 填充内容
                    modelContent.setContent(content);
                    logEntity.setAssistantMessage(content);
                    
                    // 保存日志和内容
                    saveLog(logEntity);
                    saveContent(modelContent, keyBase + RoleRedisEnum.ROLE_MODEL.getName());
                    
                    log.info("✅ Mono 异步响应已保存");
                })
                .doOnError(e -> {
                    // 异常处理：仍然保存日志，标记为失败
                    logEntity.setAssistantMessage("Mono 异步失败：" + e.getMessage());
                    saveLog(logEntity);
                    log.error("❌ Mono 处理异常", e);
                })
                .doFinally(sig -> log.info("⏱️ Mono 结束，耗时：{}ms", System.currentTimeMillis() - start));
    }

    /**
     * 处理 Flux 流式异步响应
     * 
     * Flux 适用场景：
     * - 流式输出（如逐字返回）
     * - 多个异步响应
     * 
     * 处理流程：
     * 1. 收集所有流式数据到 List
     * 2. 拼接成完整字符串（用空格分隔）
     * 3. 保存到数据库和 Redis
     * 4. 转回 Flux 继续流式返回给前端
     * 
     * 注意：
     * - collectList() 会等待所有数据到达（大数据流可能内存压力大）
     * - 前端仍然是流式接收（不影响用户体验）
     * - 失败不会中断响应
     * 
     * @param flux 异步流式响应流
     * @param logEntity AI 日志对象
     * @param modelContent AI 回复对象
     * @param keyBase Redis key 前缀
     * @param start 开始时间（用于计算耗时）
     * @return 包装后的 Flux（添加了日志保存逻辑）
     */
    private Flux<?> handleFluxResponse(Flux<?> flux, ChatAiLog logEntity, ChatContent modelContent, String keyBase, long start) {
        return flux.collectList()  // 收集所有流式数据
                .doOnSuccess(list -> {
                    // 拼接成完整字符串（用空格分隔）
                    String joined = list.stream()
                            .map(Object::toString)
                            .collect(Collectors.joining(" "));
                    
                    log.info("🤖 大模型回复内容：{}", joined);
                    
                    // 填充内容
                    modelContent.setContent(joined);
                    logEntity.setAssistantMessage(joined);
                    
                    // 保存日志和内容
                    saveLog(logEntity);
                    saveContent(modelContent, keyBase + RoleRedisEnum.ROLE_MODEL.getName());
                    
                    log.info("✅ Flux 异步响应已保存");
                })
                .flatMapMany(Flux::fromIterable)  // 转回 Flux 继续流式返回
                .doFinally(sig -> log.info("⏱️ Flux 结束，耗时：{}ms", System.currentTimeMillis() - start));
    }

    /**
     * 处理普通同步返回结果
     * 
     * 适用场景：
     * - 非异步响应（如 String、对象等）
     * - 简单的同步调用
     * 
     * 处理流程：
     * 1. 直接获取返回值
     * 2. 转为字符串
     * 3. 保存到数据库和 Redis
     * 
     * 注意：
     * - 同步保存（会阻塞当前线程）
     * - 适用于非高并发场景
     * 
     * @param result 方法返回值
     * @param logEntity AI 日志对象
     * @param modelContent AI 回复对象
     * @param keyBase Redis key 前缀
     */
    private void handleNormalResponse(Object result, ChatAiLog logEntity, ChatContent modelContent, String keyBase) {
        // 转为字符串
        String message = result != null ? result.toString() : "（无内容）";
        log.info("🤖 大模型回复内容：{}", message);
        
        // 填充内容
        modelContent.setContent(message);
        logEntity.setAssistantMessage(message);
        
        // 保存日志和内容
        saveLog(logEntity);
        saveContent(modelContent, keyBase + RoleRedisEnum.ROLE_MODEL.getName());
    }
}
